(function () {

	var SerializedImage = new XMLSerializer().serializeToString(document.createElement("img")),
		//Chrome does not add XML namespace automatically in XMLSerializer
		SupportsAutomaticNS = SerializedImage.indexOf("xmlns") > -1,
		//Chrome does not close autoclosing tags
		SupportsAutocloseTags = SerializedImage.indexOf("/") > -1,
		//Firefox does not fill CSSText in the object returned by getComputedStyle
		SupportsCopyFromCssText = getComputedStyle(document.createElement("div")).cssText !== "",
		//Firefox returns wrong value for margins if the element has margin:auto
		SupportsMarginAuto = null,
		//Properties to consider when checking margin:auto
		MarginProps = {"margin":0, "marginLeft":0, "marginRight":0},
		//Regexp to match autoclosing tags that are not closed
		UnclosedTags = /(<(?:area|basefont|base|br|hr|input|img|link|meta|param)(?:\s+[\w\-]+="[^"]*")*\s*)>/gi,
		//Regexp to match css rules to skip when cloning style. This includes print styles rules that are useless
		SkipCSS = /^(cssText|[0-9]+|border(?:Color|Width|Style|Right|Left|Top|Bottom)|cursor|widows|marks|orphans|page|pageBreak(?:After|Before|Inside))$/,
		//CSS properties that may contain images
		ImageInCSS = ["backgroundImage", "borderImageSource", "listStyleImage"],
		//Regexp to match css resources loaded by url()
		CSSUrl = /url\("?(.*?)"?\)/g,
		//Regex to check if a string is an absolute url
		AbsoluteUrl = /^http(?:s)?:\/\//,
		//Converts a string to base64
		SafeToBase64String = function (str) {
			return btoa(unescape(encodeURIComponent(str)));
		},
		//Calculates the position of an element
		GetPosition = function (el) {
			var rect = el.getBoundingClientRect(),
				doc = el.ownerDocument,
				win = doc.defaultView,
				docEl = doc.documentElement,
				body = doc.body;			
			return {
				x: rect.left + (win.pageXOffset || docEl.scrollLeft || body.scrollLeft),
				y: rect.top + (win.pageYOffset || docEl.scrollTop || body.scrollTop)
			};
		},
		//Converts an image element to its base64 representation
		ImageToBase64 = function (img) {
			var canvas = document.createElement("canvas");
			canvas.width = img.width;
			canvas.height = img.height;
			var ctx = canvas.getContext("2d");
			ctx.drawImage(img, 0, 0);
			try {
				return canvas.toDataURL("image/png");
			} catch (e) {
				return "";
			}
		},
		//Returns an array that contains the element and all its children
		GetStaticTree = function (el) {
			return [el].concat(Array.prototype.slice.call(el.getElementsByTagName("*")));
		},
		//Disconnects the given element and its sub-tree from the main tree
		Disconnect = function (el) {
			var elsToDisconnect = GetStaticTree(el);
			for (var i = 0, l = elsToDisconnect.length; i < l; i++) {
				if (elsToDisconnect[i].parentNode) {
					elsToDisconnect[i].parentNode.removeChild(elsToDisconnect[i]);
				}
			}
		},
		//Checks margin properties in stylesheets to solve margin:auto issue
		CheckMarginInStyleSheet = function (sheet, origEl, win) {
		
			//If the stylesheet is disabled or his media text does not match don't process it
			if (sheet.disabled || !win.matchMedia(sheet.media.mediaText).matches) {
				return;
			}
			
			//Access to cssRules in cross-domain stylesheets throws an exception
			try {
				//Loop cssRules
				var rules = sheet.cssRules;
				for (var r = 0, rl = rules.length; r < rl; r++) {
					var rule = rules[r], margin;
					//Analyze the rule only if it's a style rule
					if (rule.type === rule.STYLE_RULE) {
						for (m in MarginProps) {
							margin = rule.style[m];
							//Check if it contains margin:auto
							if (margin !== "" && margin.indexOf("auto") >= 0) {
								//Check elements that match the rule's selector text
								var sel = origEl.querySelectorAll(rule.selectorText);
								for (var s = 0, slen = sel.length; s < slen; s++) {
									//Apply the margin in the style if it's not already present
									if (sel[s].style[m] === "") {
										sel[s].style[m] = margin;
									}
								}
							}
						}
					} else if (rule.type === rule.IMPORT_RULE) {
						//If it's an import rule  check its stylesheet
						CheckMarginInStyleSheet(rule.styleSheet, origEl, win);
					}
				}
			} catch(e) {}
		},
		//Create an element that replaces the current one and adopts all of its children
		CreateReplacer = function (el, target) {
			var replacer = el.ownerDocument.createElement("div");
			//Avoid margin collapse
			if (el.tagName === "HTML") {
				el.style.overflow = "auto";
			}
			replacer.style.cssText = el.style.cssText;
			while (el.hasChildNodes()) {
				replacer.appendChild(el.childNodes[0]);
			}
			el.parentNode && el.parentNode.replaceChild(replacer, el);
			return replacer;
		},
		//Creates an empty element that is a placeholder for another one
		CreatePlaceholder = function (el, target, placeholder) {
			if (!placeholder) {
				placeholder = el.ownerDocument.createElement("div");
			}
			placeholder.style.cssText = el.style.cssText;
			placeholder.style.width = (target.getAttribute("width") || target.offsetWidth) + "px";
			placeholder.style.height = (target.getAttribute("height") || target.offsetHeight) + "px";
			placeholder.style.display = "inline-block";
			el.parentNode && el.parentNode.replaceChild(placeholder, el);
			Disconnect(el);
			return placeholder;
		},
		//Applies borders to frames
		ApplyFrameBorder = function (el, target) {
			var borderWidth = 6;
			if (target.hasAttribute("_bv")) {
				el.style.borderLeftWidth = borderWidth + "px";
				el.style.borderLeftStyle = "solid";
				el.style.borderLeftColor = "#999";
				el.style.width = (parseInt(el.style.width) - borderWidth) + "px";
				target.removeAttribute("_bv");
			}
			if (target.hasAttribute("_bh")) {
				el.style.borderTopWidth = borderWidth + "px";
				el.style.borderTopStyle = "solid";
				el.style.borderTopColor = "#999";
				el.style.height = (parseInt(el.style.height) - borderWidth) + "px";
				target.removeAttribute("_bh");
			}
			if (target.parentNode.tagName === "FRAMESET") {
				el.style.display = "inline-block";
			}
			return el;
		},
		//Processing function for particular tags
		Process = {
			"HTML": CreateReplacer,
			"BODY": CreateReplacer,
			"FRAMESET": function (el, target) {
				var cols = target.cols.split(",").length,
					rows = target.rows.split(",").length,
					index = 0,
					children = target.children;
				for (var r = 0; r < rows; r++) {
					for (var c = 0; c < cols; c++) {
						c && children[index].setAttribute("_bv", "1");
						r && children[index].setAttribute("_bh", "1");
						index++;
					}
				}
				return ApplyFrameBorder(CreateReplacer(el, target), target);
			},
			"IMG": function (el, target) {
				//Add the image as a resource
				this.AddResource("images", el, el.src);
				return el;
			},
			"INPUT": function (el, target) {
				if (el.type === "checkbox" || el.type === "radio") {
					//For checkboxes and radios set the 'checked' attribute
					if (target.checked) {
						el.setAttribute("checked", "cheched");
					} else {
						el.removeAttribute("checked");
					} 
				} else {
					//For other input types set the value
					el.setAttribute("value", target.value);
				}
				return el;
			},
			"SELECT": function (el, target) {
				//Select all selected options
				for (var i = 0, l = target.options.length; i < l; i++) {
					if (target.options[i].selected) {
						el.options[i].setAttribute("selected", "selected");
					} else {
					el.options[i].removeAttribute("selected");
					}
				}
				return el;
			},
			"TEXTAREA": function (el, target) {
				el.textContent = target.value;
				return el;
			},
			"CANVAS": function (el, target) {
				try {
					var dataUri = target.toDataURL("image/png"),
						img = el.ownerDocument.createElement("img");
					img.src = dataUri;
					return CreatePlaceholder(el, target, img);
				} catch (e) {
					return CreatePlaceholder(el, target);
				}
			},
			"IFRAME": function (el, target) {
				try {
					//Check if contentDocument is accessible
					if (this.IsCrossDomain(target.src) || !target.contentDocument) {
						//Throw error to execute the catch statement
						throw new Error();
					}
					this.AddResource("frames", [el, target], el.src);
					return el;
				} catch (e) {
					//If the contentDocument is not accessible simply remove the source
					el.removeAttribute("src");
					return el;
				}
			},
			"FRAME": function (el, target) {
				try {
					//Check if contentDocument is accessible
					if (this.IsCrossDomain(target.src) || !target.contentDocument) {
						//Throw error to execute the catch statement
						throw new Error();
					}
					this.AddResource("frames", [ApplyFrameBorder(el, target), target], el.src);
					return el;
				} catch (e) {
					//If the contentDocument is not accessible simply remove the source
					return ApplyFrameBorder(CreatePlaceholder(el, target), target);
				}
			},
			"APPLET": CreatePlaceholder,
			"EMBED": CreatePlaceholder,
			"OBJECT": CreatePlaceholder
		};
		
		//ScreenShot internal class
		var ScreenShot = function (target, ready, options, filter) {
			
			//Check that the argument is an element or a document
			if (target.nodeType !== 1 && target.nodeType !== 9) {
				throw new Error("Screenshots are allowed only on elements and documents");
			}
			
			//If target is a document use it's documentElement
			if (target.nodeType === 9) {
				target = target.documentElement;
			}
			
			if (!options) {
				options = {};
			}
			
			this.Skip = options.skip || {};
			this.Resources = {};
			this.Filter = filter || {};

			//Execute tests that require the document to be ready
			if (SupportsMarginAuto === null) {
				var div = document.createElement("div"),
					child = document.createElement("div");
				div.style.cssText = "display:block;width:4px;height:1px;position:absolute;left:-10px";
				child.style.cssText = "display:block;width:2px;height:1px;margin:auto";
				div.appendChild(child);
				document.body.appendChild(div);
				SupportsMarginAuto = parseInt(getComputedStyle(child).marginLeft) > 0;
				document.body.removeChild(div);
				div = child = null;
			}
			
			//Calculate element's dimensions and clone it
			var ownerWindowLocation = target.ownerDocument.defaultView.location,
				w = options.width || target.offsetWidth,
				h = options.height || target.offsetHeight,
				el = target.cloneNode(true);
			
			//Register window URL
			this.BaseUrl = ownerWindowLocation.protocol + "//" + ownerWindowLocation.host;
			
			//Sanitize the clone element
			el = this.FixElement(el, target);
			
			//Add the XML namespace only if the XMLSerializer doesn't add it automatically
			!SupportsAutomaticNS && el.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
			
			//Load resources asynchronously
			this.LoadResources(function () {

				//Remove margins from the clone element
				el.style.margin = "0";

				//Compose the data uri
				var serialized = new XMLSerializer().serializeToString(el);
				
				//XMLSerializer doesn't close autoclosing tags properly on Chrome, fix it
				if (!SupportsAutocloseTags) {
					serialized = serialized.replace(UnclosedTags, function (a, tag) {return tag + "/>";});
				}
				
				//Compose SVG filters
				var SVGFilterId = "__AeroBlurFilter",
					SVGFilterProp = "",
					SVGFilter = "";
				
				if (this.Filter.blur) {
					SVGFilter += "<feGaussianBlur in='SourceGraphic' stdDeviation='" + this.Filter.blur + "'/>";
				}
				
				if (SVGFilter) {
					SVGFilter = "<defs>" +
									"<filter id='" + SVGFilterId + "' x='0' y='0'>" +
										SVGFilter +
									"</filter>" +
								"</defs>";
					SVGFilterProp = " filter='url(#" + SVGFilterId + ")'";
				}
				
				//Compose the dataUri
				var dataUri = "data:image/svg+xml;charset=utf-8;base64," + 
							  SafeToBase64String(
								"<svg xmlns='http://www.w3.org/2000/svg' width='" + w + "' height='" + h + "'>" +
									SVGFilter +
									"<foreignObject width='100%' height='100%'" + SVGFilterProp + ">" +
										serialized +
									"</foreignObject>" +
								"</svg>"
							  );
				
				if (options.passDataUri) {
					ready.call(options.bind, dataUri);
				} else {
					//Create the resulting image
					var img = new Image();
					img.onload = function () {
						ready.call(options.bind || this, this);
					};
					img.src = dataUri;
				}
				
			});
		};
		
		ScreenShot.prototype = {
			//ScreenShot target element's base url
			BaseUrl: "",
			//ScreenShot filters
			Filter: null,
			//Resource collection
			Resources: null,
			//Object that contains resources type that will be skipped
			Skip: {},
			//Register a resource to be loaded
			AddResource: function (type, el, src, group) {
				//Check if the url is already in dataUri format
				if (!this.Skip[type] && src.indexOf("data:") !== 0) {
					if (!this.Resources[type]) {
						this.Resources[type] = {};
					}
					if (!this.Resources[type][src]) {
						this.Resources[type][src] = {els: [], group: {}};
					}
					if (!group) {
						this.Resources[type][src].els.push(el);
					} else {
						if (!this.Resources[type][src].group[group]) {
							this.Resources[type][src].group[group] = [];
						}
						this.Resources[type][src].group[group].push(el);
					}
				}
			},
			//Checks if a path is cross domain, relative to the target's base url
			IsCrossDomain: function (path) {
				return AbsoluteUrl.test(path) && path.indexOf(this.BaseUrl) !== 0;
			},
			//Fixes clone element and its sub tree
			FixElement: function (origEl, origTarget) {
				var sl = ImageInCSS.length,
					doc = origTarget.ownerDocument,
					win = doc.defaultView,
					match,
					els = GetStaticTree(origEl),
					targets =  GetStaticTree(origTarget);

				//The only way to solve the margin:auto issue is to look for any css margin rule in stylesheets
				if (!SupportsMarginAuto) {
					var ss = doc.styleSheets;
					for (var i = 0, l = ss.length; i < l; i++) {
						CheckMarginInStyleSheet(ss[i], origEl, win);
					}
				}
				
				for (var i = 0, l = els.length; i < l; i++) {
					
					var el = els[i],
						target = targets[i],
						tag = el.tagName;
					
					//Skip disconnected nodes
					if (!el.parentNode && i) {
						continue;
					}
					
					var computedStyle = win.getComputedStyle(target);
					
					//Don't process hidden elements. This should also remove dangerous elements such as scripts
					if (computedStyle.display === "none") {
					
						//Disconnect the element and all its child nodes
						Disconnect(el);
						continue;
						
					}
					
					//Inline CSS rules. Copy CSS text if supported, otherwise copy every single property
					if (SupportsCopyFromCssText) {
						el.style.cssText = computedStyle.cssText;
					} else {
						if (!SupportsMarginAuto) {
							var margins = {};
							for (var m in MarginProps) {
								if (el.style[m] !== "" && el.style[m].indexOf("auto") >= 0) {
									margins[m] = el.style[m];
								}
							}
						}
						for (var prop in computedStyle) {
							if (typeof computedStyle[prop] === "string" && !SkipCSS.test(prop)) {
								el.style[prop] = computedStyle[prop];
							}
						}
						//Fix the margin: auto issue for inline style
						if (!SupportsMarginAuto) {
							for (var m in MarginProps) {
								el.style[m] = margins[m];
							}
						}
					}
					
					//If the element must be processed, do it
					if (Process[tag]) {
						el = els[i] = Process[tag].call(this, el, target);
					}
					
					//Collect images inside element's style if required
					if (!this.Skip.images) {
						for (var s = 0; s < sl; s++) {
							var prop = el.style[ImageInCSS[s]];
							if (prop !== "" && prop !== "none") {
								CSSUrl.lastIndex = 0;
								while (match = CSSUrl.exec(prop)) {
									this.AddResource("images", el, match[1], ImageInCSS[s]);
								}
							}
						}
					}
				
				}
				
				//The root element could be changed, return the current root
				return els[0];
			},
			//Loads all the resources
			LoadResources: function (ready) {
				var count = 0,
					self = this,
					loopEnd = false,
					execReady = function () {
						if (count === 0 && loopEnd) {
							//Empty the resources object
							self.Resources = {};
							ready.call(self);
						}
					},
					loadFnImage = function (error) {
						//Check to see if the function has been triggered by an error event
						if (error !== true) {
							
							//Sometimes dataUri images trigger the load function again, prevent this
							if (!self.Resources.images || !(this.src in self.Resources.images)) {
								return;
							}
						
							var dataUri = ImageToBase64(this);
							
							//Check if the conversion has been made
							if (dataUri !== "") {
								var res = self.Resources.images[this.src];
								
								//Apply the dataUri to all images
								for (var i = 0, l = res.els.length; i < l; i++) {
									res.els[i].src = dataUri;
								}
								
								//Apply the dataUri to all style properties
								for (var s in res.group) {
									for (var i = 0, l = res.group[s].length; i < l; i++) {
										res.group[s][i].style[s] = res.group[s][i].style[s].replace(this.src, dataUri);
									}
								}
							}
						}

						count--;
						execReady();
					};
				
				//Images and images in css
				for (var url in this.Resources.images) {
				
					//Skip cross-domain resources for now
					if (this.IsCrossDomain(url)) {
						continue;
					}
				
					if (this.Resources.images[url].els.length) {
						var img = this.Resources.images[url].els[0];
					} else {
						var img = new Image();
						img.src = url;
					}
					
					count++;
					
					if (img.complete) {
						//Check the naturalWidth to see if the image exists
						loadFnImage.call(img, img.naturalWidth === 0);
					} else {
						img.addEventListener("load", loadFnImage, false);
						img.addEventListener("error", function () {loadFnImage.call(this, true);}, false);
					}
					
				}
				
				//Frames, iframe
				for (var url in this.Resources.frames) {
					for (var i = 0, l = this.Resources.frames[url].els.length; i < l; i++) {
						var res = this.Resources.frames[url].els[i];
						count++;
						new ScreenShot(res[1].contentDocument, function (src) {
							var img = new Image();
							img.style.cssText = this.style.cssText;
							img.src = src;
							this.parentNode.replaceChild(img, this);
							count--;
							execReady();
						}, {bind: res[0], passDataUri: true, width: res[1].offsetWidth, height: res[1].offsetHeight});
					}
				}
				
				loopEnd = true;
				execReady();
			}
		};
		
	/**
	 * @class Aero
	 * @constructor
	 * @param element 	{Object} 	Element that will get the background.
	 * @param [options] {Object}	Instance options. Available options are:
	 *								- ready {Function} Function called when the background
	 *								  is ready
	 *								- blur {Number} Blur amount
	 */
	window.Aero = function (element, options) {
		this.element = element;
		this.options = options || {};
		this.update();
	};
	
	window.Aero.prototype = {
		/**
		 * Element that will get the background.
		 * 
		 * @property element
		 * @type {Object}
		 */
		element: null,
		/**
		 * Instance options
		 * 
		 * @property options
		 * @type {Object}
		 */
		options: null,
		/**
		 * Updates the background of the element.
		 *
		 * @method update
		 */
		update: function () {
			var currVis = this.element.style.visibility,
				ownerDoc = this.element.ownerDocument,
				bodyBg = ownerDoc.defaultView.getComputedStyle(ownerDoc.body).backgroundColor;
			
			//Find the right background color. It will be applied to the element because the
			//background of the screenshot will be transparent and it will cause a
			//semi-transparent blur effect when applied if element's background is transparent
			//too
			if (bodyBg === "transparent" || bodyBg === "rgba(0, 0, 0, 0)") {
				bodyBg = "white";
			}
			
			//Make the element invisible before taking the screenshot of the document
			this.element.style.visibility = "hidden";
			
			new ScreenShot(ownerDoc, function (src) {
				this.element.style.backgroundRepeat = "no-repeat";
				this.element.style.backgroundImage = "url(" + src + ")";
				this.element.style.backgroundColor = bodyBg;
				this.element.style.visibility = currVis;
				this.updatePosition();
				this.options.ready && this.options.ready();
			}, {bind: this, passDataUri: true}, {blur: this.options.blur || 5});
		},
		/**
		 * Updates the background position on the element.
		 *
		 * @method updatePosition
		 */
		updatePosition: function () {
			var pos = GetPosition(this.element),
				computedStyle = this.element.ownerDocument.defaultView.getComputedStyle(this.element),
				borderTop = parseInt(computedStyle.borderTopWidth),
				borderLeft = parseInt(computedStyle.borderLeftWidth);
			this.element.style.backgroundPosition = (-pos.x - borderLeft) + "px " + (-pos.y - borderTop) + "px";
		}
	};
	
	/**
	 * Takes a screenshot of the given element.
	 *
	 * @method ScreenShot
	 * @param target	{Object} 	Element for the screenshot.
	 * @param ready		{Function} 	Function that will be executed when the screenshot is ready. It receives one.
	 *								argument that depends on the passDataUri argument.
	 * @param [options] {Object}	Options object. Available options are:
	 *								- bind {Object}	object that will be the 'this' of the ready function.
	 *								- passDataUri {Boolean} If it's true the ready function will receive
	 *								  a data-uri string representing the screenshot, otherwise the data-uri
	 *								  will be loaded as an image and the argument of the function will be
	 *								  that image.
	 *								- width: Width of the screenshot, by default it uses target element's width.
	 *								- height: Height of the screenshot, by default it uses target element's height.
	 *								- skip: object in which the keys represents a type of resource that won't be
	 *								  parsed if the value is true. Available keys are "images", "frames" and "fonts".
	 * @static
	 */
	window.Aero.ScreenShot = function (target, ready, options) {
		new ScreenShot(target, ready, options);
	};
	
	/**
	 * Check if the browser can use the library
	 *
	 * @method Support
	 * @static
	 */
	window.Aero.Support = function () {
		//Check SVG images and foreignObject support
		return document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image", "1.1") &&
			   typeof SVGForeignObjectElement !== 'undefined';
	};
	
}());